β

Как стать автором
Погнали в космос?
Айтишники, аттеншн! Выберите лучший IT-бренд 2025

Моя лентаВсе потоки

divan0 18 авг 2016 в 19:40

Как устроены каналы в Go

4 мин

75K

Go*

Перевод

Автор оригинала: Dmitry Vorobev

Перевод познавательной статьи "Golang: channels implementation" о том, как устроены каналы в Go.

Go становится всё популярнее и популярнее, и одна из причин этого — великолепная поддержка конкурентного программирования. Каналы и горутины сильно упрощают разработку конкурентных программ. Есть несколько хороших статей о том, как реализованы различные структуры данных в Go — к примеру, слайсы, карты, интерфейсы — но про внутреннюю реализацию каналов написано довольно мало. В этой статье мы изучим, как работают каналы и как они реализованы изнутри. (Если вы никогда не использовали каналы в Go, рекомендую сначала прочитать эту статью.)

Устройство канала

Давайте начнём с разбора структуры канала:

qcount — количество элементов в буфере

dataqsiz — размерность буфера

buf — указатель на буфер для элементов канала

closed — флаг, указывающий, закрыт канал или нет

recvq — указатель на связанный список горутин, ожидающих чтения из канала

sendq -указатель на связанный список горутин, ожидающих запись в канал

lock — мьютекс для безопасного доступа к каналу

В общем случае, горутина захватывает мьютекс, когда совершает какое-либо действие с каналом, кроме случаев lock-free проверок при неблокирующих вызовах (я объясню это подробнее чуть ниже). Closed — это флаг, который устанавливается в 1, если канал закрыт, и в 0, если не закрыт. Эти поля далее будут исключены из общей картины, для большей ясности.

Канал может быть синхронным (небуферизированным) или асинхронным (буферезированным). Давайте вначале посмотрим, как работают синхронные каналы.

Синхронные каналы

Допустим, у нас есть следующий код:

package main

func main() {
ch := make(chan bool)
go func() {
ch <- true
}()
<-ch
}

Вначале создается новый канал и он выглядит вот так:

Go не выделяет буфер для синхронных каналов, поэтому указатель на буфер равен nil и dataqsiz равен нулю. В приведённом коде нет гарантии, что случится первее — чтение из канала или запись, поэтому допустим, что первым действием будет чтение из канала (обратный пример, когда вначале идёт запись, будет рассмотрена ниже в примере с буферизированным каналами). Вначале, текущая горутина произведёт некоторые проверки, такие как: закрыт ли канал, буферизирован он или нет, содержит ли гоуртины в send-очереди. В нашем примере у канала нет ни буфера, ни ожидающих отправки горутин, поэтому горутина добавит сама себя в recvq и заблокируется. На этом шаге наш канал будет выглядеть следующим образом:

Теперь у нас осталась только одна работающая горутина, которая пытается записать данные в канал. Все проверки повторяются снова, и когда горутина проверяет recvq очередь, она находит ожидающую чтение горутину, удаляет её из очереди, записывает данные в её стек и снимает блокировку. Это единственное место во всём рантайме Go, когда одна горутина пишет напрямую в стек другой горутины. После этого шага, канал выглядит точно так же, как сразу после инициализации. Обе горутины завершаются и программа выходит.

Так устроены синхронные каналы. Сейчас же, давайте посмотрим на буферизированные каналы.

Буферезированные каналы

Рассмотрим следующий пример:

package main

func main() {
ch := make(chan bool, 1)
ch <- true
go func() {
<-ch
}()
ch <- true
}

Опять же, порядок исполнения неизвестен, пример с первой читающей горутиной мы разобрали выше, поэтому сейчас допустим, что два значения были записаны в канал, и после этого один из элементов вычитан. И первым шагом идёт создание канала, который будет выглядеть вот так:

Разница в сравнении с синхронным каналом в том, что тут Go выделяет буфер и устанавливает значение dataqsiz в единицу.

Следующим шагом будет отправка первого значения в канал. Чтобы сделать это, горутина сначала производит несколько проверок: пуста ли очередь recvq, пуст ли буфер, достаточно ли места в буфере.

В нашем случае в буфере достаточно места и в очереди ожидания чтения нет горутин, поэтому горутина просто записывает элемент в буфер, увеличивает значение qcount и продолжает исполнение далее. Канал в этот момент выглядит так:

На следующем шаге, горутина main отправляет следующее значение в канал. Когда буфер полон, буферизированный канал будет вести себя точно так же, как синхронный (небуферизированный) канал, тоесть горутина добавит себя в очередь ожидания и заблокируется, в результате чего, канал будет выглядеть следующим образом:

Сейчас горутина main заблокирована и Go запустил одну анонимную горутину, которая пытается прочесть значение из канала. И вот тут начинается хитрая часть. Go гарантирует, что канал работает по принципу FIFO очереди (спецификация), но горутина не может просто взять значение из буфера и продолжить исполнение. В этом случае горутина main заблокируется навсегда. Для решения этой ситуации, текущая горутина читает данные из буфера, затем добавляет значение из заблокированной горутины в буфер, разблокирует ожидающую горутину и удаляет её из очереди ожидания. (В случае же, если нет ожидающих горутину, она просто читает первое значение из буфера)

Select

Но постойте, Go же ещё поддерживает select с дефолтным поведением, и если канал заблокирован, как горутина сможет обработать default? Хороший вопрос, давайте быстро посмотрим на приватное API каналов. Когда вы запускаете следующий кусок кода:

select {
case <-ch:
foo()
default:
bar()
}

Go запускает функцию со следующей сигнатурой:

func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool)

chantype это тип канала (например, bool в случае make(chan bool)), hchan — указатель на структуру канала, ep — указатель на сегмент памяти, куда должны быть записаны данные из канала, и последний, но самый интересный для нас — это аргумент block. Если он установлен в false, то функция будет работать в неблокирующем режиме. В этом режиме горутина проверяет буфер и очередь, возвращает true и пишет данные в ep или возвращает false, если нет данных в буфере или нет отправителей в очереди. Проверки буфера и очереди реализованы как атомарные операции, и не требуют блокировки мьютекса.

Также есть функция для записи данных в очередь с аналогичной сигнатурой.

Мы разобрались как работают запись и чтение из канала, давайте теперь взглянём, что происходит при закрытии канала.

Закрытие канала

Закрытие канала это простая операция. Go проходит по всем ожидающим на чтение или запись горутинам и разблокирует их. Все получатели получают дефолтные значение переменных того типа данных канала, а все отправители паникуют.

Заключение

В этой статье мы рассмотрели, как каналы реализованы и как работают. Я постарался описать их как можно проще, поэтому упустил некоторые детали. Задача статьи — предоставить базовое понимание внутреннего устройства каналов и подтолкнуть вас к чтениею исходных кодов Go, если вы хотите получить более глубокое понимание. Просто почитайте код реализации каналов. Мне он кажется очень простым, хорошо документированным и довольно коротким, всего около 700 строк кода.

Ссылки

Исходный код

Каналы в спецификации Go

Каналы Go на стероидах

Теги:

go

channels

Хабы:

Go

+30

284

11
+11

192

Карма

0

Рейтинг

divan0 @divan0

Пользователь

Подписаться

Комментарии 11
+11

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.2K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.7K

Кейс

Перевод

+27

21

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.5K

Мнение

+22

17

63
+63

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

25

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.2K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.7K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.6K

+16

5

6
+6

psygalaxy 14 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 17 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

AI и майндсет айтишников — выясняем, как изменились IT-работодатели за год
Турбо

Показать еще

Минуточку внимания

Опрос

Да начнётся битва: выбираем лучший IT-бренд работодателя

Событие

Как построить IT-бренд через вирусный контент и юмор

Турбо

Делаем сисадмина сильнее с помощью менеджера паролей

Вопросы и ответы

Trace/breakpoint trap (core dumped)?
LinuxПростой2 ответа

Неточность формулировки в книге или же я не понимаю?
GoПростой2 ответа

Зачем for {select} оборачивать в еще один select с time.Ticker?
GoПростой3 ответа

Какие гарантии на выравнивание адреса указателя?
GoСредний1 ответ

Как поменять env переменную PATH в golang (MacOS)?
GoСредний2 ответа

Больше вопросов на Хабр Q&A

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.6K

12
+12

Моё мини-исследование про продолжительность жизни в России

6.5K

63
+63

GPT-5 получит модуль для борьбы с «галлюцинациями» — The Information

872

3
+3

AI и майндсет айтишников — выясняем, как изменились IT-работодатели за год
Турбо

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr